/**
 * @author Ed Spencer (http://sencha.com) Transition plugin for DataViews
 */
Ext.ux.DataViewTransition = Ext.extend(Object, {

	/**
	 * @property defaults
	 * @type Object Default configuration options for all DataViewTransition
	 *       instances
	 */
	defaults : {
		duration : 750,
		idProperty : 'id'
	},

	/**
	 * Creates the plugin instance, applies defaults
	 * 
	 * @constructor
	 * @param {Object}
	 *            config Optional config object
	 */
	constructor : function(config) {
		Ext.apply(this, config || {}, this.defaults);
	},

	/**
	 * Initializes the transition plugin. Overrides the dataview's default
	 * refresh function
	 * 
	 * @param {Ext.view.View}
	 *            dataview The dataview
	 */
	init : function(dataview) {
		/**
		 * @property dataview
		 * @type Ext.view.View Reference to the DataView this instance is bound
		 *       to
		 */
		this.dataview = dataview;

		var idProperty = this.idProperty;
		dataview.blockRefresh = true;
		dataview.updateIndexes = Ext.Function.createSequence(
				dataview.updateIndexes, function() {
					this.getTargetEl().select(this.itemSelector).each(
							function(element, composite, index) {
								element.id = element.dom.id = Ext.util.Format
										.format("{0}-{1}", dataview.id,
												dataview.store.getAt(index)
														.get(idProperty));
							}, this);
				}, dataview);

		/**
		 * @property dataviewID
		 * @type String The string ID of the DataView component. This is used
		 *       internally when animating child objects
		 */
		this.dataviewID = dataview.id;

		/**
		 * @property cachedStoreData
		 * @type Object A cache of existing store data, keyed by id. This is
		 *       used to determine whether any items were added or removed from
		 *       the store on data change
		 */
		this.cachedStoreData = {};

		// var store = dataview.store;

		// catch the store data with the snapshot immediately
		this.cacheStoreData(dataview.store.snapshot);

		dataview.store.on('datachanged', function(store) {
			var parentEl = dataview.getTargetEl(), calcItem = store.getAt(0), added = this
					.getAdded(store), removed = this.getRemoved(store), previous = this
					.getRemaining(store), existing = Ext.apply({}, previous,
					added);

			// hide old items
			Ext.each(removed, function(item) {
						Ext.fly(this.dataviewID + '-'
								+ item.get(this.idProperty)).animate({
									remove : false,
									duration : duration,
									opacity : 0,
									useDisplay : true
								});
					}, this);

			// store is empty
			if (calcItem == undefined) {
				this.cacheStoreData(store);
				return;
			}

			var el = Ext.get(this.dataviewID + "-"
					+ calcItem.get(this.idProperty));

			// calculate the number of rows and columns we have
			var itemCount = store.getCount(), itemWidth = el.getMargin('lr')
					+ el.getWidth(), itemHeight = el.getMargin('bt')
					+ el.getHeight(), dvWidth = parentEl.getWidth(), columns = Math
					.floor(dvWidth / itemWidth), rows = Math.ceil(itemCount
					/ columns), currentRows = Math.ceil(this.getExistingCount()
					/ columns);

			// make sure the correct styles are applied to the parent element
			parentEl.applyStyles({
						display : 'block',
						position : 'relative'
					});

			// stores the current top and left values for each element
			// (discovered below)
			var oldPositions = {}, newPositions = {}, elCache = {};

			// find current positions of each element and save a reference in
			// the elCache
			Ext.iterate(previous, function(id, item) {
						var id = item.get(this.idProperty), el = elCache[id] = Ext
								.get(this.dataviewID + '-' + id);

						oldPositions[id] = {
							top : el.getY() - parentEl.getY()
									- el.getMargin('t')
									- parentEl.getPadding('t'),
							left : el.getX() - parentEl.getX()
									- el.getMargin('l')
									- parentEl.getPadding('l')
						};
					}, this);

			// set absolute positioning on all DataView items. We need to set
			// position, left and
			// top at the same time to avoid any flickering
			Ext.iterate(previous, function(id, item) {
						var oldPos = oldPositions[id], el = elCache[id];

						if (el.getStyle('position') != 'absolute') {
							elCache[id].applyStyles({
										position : 'absolute',
										left : oldPos.left + "px",
										top : oldPos.top + "px",

										// we set the width here to make
										// ListViews work correctly. This is not
										// needed for DataViews
										width : el.getWidth(!Ext.isIE
												|| Ext.isStrict),
										height : el.getHeight(!Ext.isIE
												|| Ext.isStrict)
									});
						}
					});

			// get new positions
			var index = 0;
			Ext.iterate(store.data.items, function(item) {
				var id = item.get(idProperty), el = elCache[id];

				var column = index % columns, row = Math.floor(index / columns), top = row
						* itemHeight, left = column * itemWidth;

				newPositions[id] = {
					top : top,
					left : left
				};

				index++;
			}, this);

			// do the movements
			var startTime = new Date(), duration = this.duration, dataviewID = this.dataviewID;

			var doAnimate = function() {
				var elapsed = new Date() - startTime, fraction = elapsed
						/ duration;

				if (fraction >= 1) {
					for (var id in newPositions) {
						Ext.fly(dataviewID + '-' + id).applyStyles({
									top : newPositions[id].top + "px",
									left : newPositions[id].left + "px"
								});
					}

					Ext.TaskManager.stop(task);
				} else {
					// move each item
					for (var id in newPositions) {
						if (!previous[id])
							continue;

						var oldPos = oldPositions[id], newPos = newPositions[id], oldTop = oldPos.top, newTop = newPos.top, oldLeft = oldPos.left, newLeft = newPos.left, diffTop = fraction
								* Math.abs(oldTop - newTop), diffLeft = fraction
								* Math.abs(oldLeft - newLeft), midTop = oldTop > newTop
								? oldTop - diffTop
								: oldTop + diffTop, midLeft = oldLeft > newLeft
								? oldLeft - diffLeft
								: oldLeft + diffLeft;

						Ext.fly(dataviewID + '-' + id).applyStyles({
									top : midTop + "px",
									left : midLeft + "px"
								});
					}
				}
			};

			var task = {
				run : doAnimate,
				interval : 20,
				scope : this
			};

			Ext.TaskManager.start(task);

			// <debug>
			var count = 0;
			for (var k in added) {
				count++;
			}
			if (Ext.global.console && Ext.global.console.log) {
				Ext.global.console.log('added:', count);
			}
			// </debug>

			// show new items
			Ext.iterate(added, function(id, item) {
						Ext.fly(this.dataviewID + '-'
								+ item.get(this.idProperty)).applyStyles({
							top : newPositions[item.get(this.idProperty)].top
									+ "px",
							left : newPositions[item.get(this.idProperty)].left
									+ "px"
						});

						Ext.fly(this.dataviewID + '-'
								+ item.get(this.idProperty)).animate({
									remove : false,
									duration : duration,
									opacity : 1
								});
					}, this);

			this.cacheStoreData(store);
		}, this);
	},

	/**
	 * Caches the records from a store locally for comparison later
	 * 
	 * @param {Ext.data.Store}
	 *            store The store to cache data from
	 */
	cacheStoreData : function(store) {
		this.cachedStoreData = {};

		store.each(function(record) {
					this.cachedStoreData[record.get(this.idProperty)] = record;
				}, this);
	},

	/**
	 * Returns all records that were already in the DataView
	 * 
	 * @return {Object} All existing records
	 */
	getExisting : function() {
		return this.cachedStoreData;
	},

	/**
	 * Returns the total number of items that are currently visible in the
	 * DataView
	 * 
	 * @return {Number} The number of existing items
	 */
	getExistingCount : function() {
		var count = 0, items = this.getExisting();

		for (var k in items)
			count++;

		return count;
	},

	/**
	 * Returns all records in the given store that were not already present
	 * 
	 * @param {Ext.data.Store}
	 *            store The updated store instance
	 * @return {Object} Object of records not already present in the dataview in
	 *         format {id: record}
	 */
	getAdded : function(store) {
		var added = {};

		store.each(function(record) {
			if (this.cachedStoreData[record.get(this.idProperty)] == undefined) {
				added[record.get(this.idProperty)] = record;
			}
		}, this);

		return added;
	},

	/**
	 * Returns all records that are present in the DataView but not the new
	 * store
	 * 
	 * @param {Ext.data.Store}
	 *            store The updated store instance
	 * @return {Array} Array of records that used to be present
	 */
	getRemoved : function(store) {
		var removed = [];

		for (var id in this.cachedStoreData) {
			if (store.findExact(this.idProperty, Number(id)) == -1) {
				removed.push(this.cachedStoreData[id]);
			}
		}

		return removed;
	},

	/**
	 * Returns all records that are already present and are still present in the
	 * new store
	 * 
	 * @param {Ext.data.Store}
	 *            store The updated store instance
	 * @return {Object} Object of records that are still present from last time
	 *         in format {id: record}
	 */
	getRemaining : function(store) {
		var remaining = {};

		store.each(function(record) {
			if (this.cachedStoreData[record.get(this.idProperty)] != undefined) {
				remaining[record.get(this.idProperty)] = record;
			}
		}, this);

		return remaining;
	}
});
